import glob
import io
import itertools
import json
import logging
import os
import re
import stat
import subprocess
from collections import namedtuple

from django.conf import settings

from tldextract import extract

from mobsf.MobSF.utils import (
    append_scan_status,
    find_java_binary,
    is_file_exists,
    is_internet_available,
    update_local_db,
)

logger = logging.getLogger(__name__)


class Trackers:
    def __init__(self, checksum, apk_dir, tools_dir):
        self.checksum = checksum
        self.apk = None
        self.apk_dir = apk_dir
        self.tracker_db = os.path.join(
            settings.SIGNATURE_DIR,
            'exodus_trackers')
        self.signatures = None
        self.nb_trackers_signature = 0
        self.compiled_tracker_signature = None
        self.compiled_network_tracker_sig = None
        self.classes = None
        self.tools_dir = tools_dir
        self._update_tracker_db()

    def _update_tracker_db(self):
        """Update Trackers DB."""
        try:
            if not is_internet_available():
                logger.warning('No Internet Connection. '
                               'Skipping Trackers Database Update.')
                return
            exodus_db = '{}/api/trackers'.format(settings.EXODUS_URL)
            resp = update_local_db('Trackers',
                                   exodus_db,
                                   self.tracker_db)
            # Check1: SHA256 Change
            if resp:
                # DB needs update
                # Check2: DB Syntax Changed
                data = json.loads(resp.decode('utf-8', 'ignore'))
                is_db_format_good = False
                if 'trackers' in data:
                    if '1' in data['trackers']:
                        if 'code_signature' in data['trackers']['1']:
                            is_db_format_good = True
                if is_db_format_good:
                    # DB Format is not changed. Let's update DB
                    msg = 'Updating Trackers Database....'
                    logger.info(msg)
                    append_scan_status(self.checksum, msg)
                    with open(self.tracker_db, 'wb') as wfp:
                        wfp.write(resp)
                else:
                    desc = (
                        'Trackers Database format from '
                        'reports.exodus-privacy.eu.org has changed.'
                        ' Database is not updated. '
                        'Please report to: https://github.com/MobSF/'
                        'Mobile-Security-Framework-MobSF/issues'
                    )
                    logger.info(desc)
                    append_scan_status(
                        self.checksum,
                        'Tracker Database format changed',
                        desc)
        except Exception as exp:
            msg = '[ERROR] Trackers DB Update'
            logger.exception(msg)
            append_scan_status(self.checksum, msg, repr(exp))

    def _compile_signatures(self):
        """
        Compile Signatures.

        Compiles the regex associated to each signature, in order to speed up
        the trackers detection.
        :return: A compiled list of signatures.
        """
        self.compiled_tracker_signature = []
        self.compiled_network_tracker_sig = []
        try:
            self.compiled_tracker_signature = [
                re.compile(track.code_signature)
                for track in self.signatures]
            self.compiled_network_tracker_sig = [
                re.compile(track.network_signature)
                for track in self.signatures]
        except TypeError:
            logger.exception('compiling tracker signature failed')

    def load_trackers_signatures(self):
        """
        Load trackers signatures from the official Exodus database.

        :return: a dictionary of signatures.
        """
        self.signatures = []
        with io.open(self.tracker_db,
                     mode='r',
                     encoding='utf8',
                     errors='ignore') as flip:
            data = json.loads(flip.read())
        for elm in data['trackers']:
            self.signatures.append(
                namedtuple('tracker',
                           data['trackers'][elm].keys())(
                               *data['trackers'][elm].values()))
        self._compile_signatures()
        self.nb_trackers_signature = len(self.signatures)

    def get_embedded_classes(self):
        """
        Get the list of Java classes from all DEX files.

        :return: list of Java classes
        """
        if self.classes is not None:
            return self.classes
        for dex_file in glob.iglob(os.path.join(self.apk_dir, '*.dex')):
            # Fix dex permissions, malware mark dex as non read/write able
            if not os.access(dex_file, os.W_OK):
                os.chmod(dex_file, stat.S_IWRITE)
            if not os.access(dex_file, os.R_OK):
                os.chmod(dex_file, stat.S_IREAD)
            if (len(settings.BACKSMALI_BINARY) > 0
                    and is_file_exists(settings.BACKSMALI_BINARY)):
                bs_path = settings.BACKSMALI_BINARY
            else:
                bs_path = os.path.join(self.tools_dir, 'baksmali-2.5.2.jar')
            args = [find_java_binary(), '-jar',
                    bs_path, 'list', 'classes', dex_file]
            try:
                classes = subprocess.check_output(
                    args, universal_newlines=True).splitlines()
                if self.classes is not None:
                    self.classes = self.classes + classes
                else:
                    self.classes = classes
            except Exception:
                pass
        return self.classes

    def detect_trackers_in_list(self, class_list, network=False):
        """
        Detect embedded trackers in the provided classes list/urls.

        :return: list of embedded trackers
        """
        if self.signatures is None:
            self.load_trackers_signatures()

        def _detect_tracker(sig, tracker, class_list):
            for clazz in class_list:
                if sig.search(clazz):
                    return tracker
            return None

        results = []
        if network:
            compiled = self.compiled_network_tracker_sig
            args = [
                (compiled[index], tracker, class_list)
                for (index, tracker) in enumerate(self.signatures) if
                len(tracker.network_signature) > 3]

        else:
            compiled = self.compiled_tracker_signature
            args = [
                (compiled[index], tracker, class_list)
                for (index, tracker) in enumerate(self.signatures) if
                len(tracker.code_signature) > 3]

        for res in itertools.starmap(_detect_tracker, args):
            if res:
                results.append(res)

        trackers = [t for t in results if t is not None]
        trackers = sorted(trackers, key=lambda trackers: trackers.name)
        return trackers

    def detect_trackers(self):
        """
        Detect embedded trackers.

        :return: list of embedded trackers
        """
        if self.signatures is None:
            self.load_trackers_signatures()
        eclasses = self.get_embedded_classes()
        if eclasses:
            return self.detect_trackers_in_list(eclasses)
        return []

    def detect_runtime_trackers(self, items, deps=False):
        """
        Detect runtime trackers.

        :return: list of embedded trackers
        """
        if self.signatures is None:
            self.load_trackers_signatures()
        if items and not deps:
            # Domains
            return self.detect_trackers_in_list(items, True)
        elif items and deps:
            # Runtime Dependencies
            return self.detect_trackers_in_list(items)
        return []

    def get_trackers(self):
        """Get Trackers."""
        msg = 'Detecting Trackers'
        logger.info(msg)
        append_scan_status(self.checksum, msg)
        trackers = self.detect_trackers()
        tracker_dict = {'detected_trackers': len(trackers),
                        'total_trackers': self.nb_trackers_signature,
                        'trackers': []}
        for trk in trackers:
            trk_url = '{}/trackers/{}'.format(settings.EXODUS_URL, trk.id)
            tracker_dict['trackers'].append({
                'name': trk.name,
                'categories': ', '.join(trk.categories),
                'url': trk_url,
            })
        return tracker_dict

    def get_trackers_domains_or_deps(self, domains, deps):
        """Get Trackers from Runtime Deps/Domains."""
        trackers = []
        tracker_dict = {
            'detected_trackers': 0,
            'total_trackers': 0,
            'trackers': []}
        msg = 'Detecting Trackers from Domains'
        logger.info(msg)
        append_scan_status(self.checksum, msg)
        # Extract Trackers from Domains
        x_domains = set()
        for d in domains:
            cps = extract(d)
            x_domains.add(f'{cps.domain}.{cps.suffix}')
        trackers = self.detect_runtime_trackers(x_domains)
        # Extract Trackers from Runtime dependencies
        if deps:
            msg = 'Detecting Trackers from Runtime dependencies'
            logger.info(msg)
            append_scan_status(self.checksum, msg)
            runtime = self.detect_runtime_trackers(deps, True)
            for i in runtime:
                if i not in trackers:
                    trackers.append(i)
        tracker_dict['detected_trackers'] = len(trackers)
        tracker_dict['total_trackers'] = self.nb_trackers_signature
        for trk in trackers:
            trk_url = '{}/trackers/{}'.format(settings.EXODUS_URL, trk.id)
            tracker_dict['trackers'].append({
                'name': trk.name,
                'categories': ', '.join(trk.categories),
                'url': trk_url,
            })
        return tracker_dict
